#region Copyright
/// <copyright>
/// Copyright (c) 2007 Ilia Shramko.
/// Released under the terms of the GNU General Public License.
/// </copyright>
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;

using Osh.Configuration;

namespace Osh.Teletext
{
	/// <summary>
	/// 
	/// </summary>
	/// <remarks>
	/// References:
	///		ETSI EN 300 706 "Enhanced Teletext specification"
	/// </remarks>
	public class TeletextDecoder
	{
		#region Tables
		private static byte[] _inversionTable = new byte[256]
		{
			0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0,
			0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
			0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8,
			0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
			0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4,
			0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
			0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec,
			0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
			0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2,
			0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
			0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea,
			0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
			0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6,
			0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
			0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee,
			0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
			0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1,
			0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
			0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9,
			0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
			0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5,
			0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
			0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed,
			0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
			0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3,
			0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
			0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb,
			0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
			0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7,
			0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
			0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef,
			0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff,
		};
		#endregion Tables

		private const int TeletextPacketLength = 45;
		private const int TeletextPacketPayloadLength = TeletextPacketLength - 3; // without Clock Run-in and Framing Code
		private TeletextService _teletextService;
		private byte[] _buffer = new byte[TeletextPacketPayloadLength];
		private DateTime _lastLoadingPagesCheck = DateTime.UtcNow;
		// Should be 100ms according to the standard, but for some channels (e.g. BBC World) this time is not enough which results in partially loaded pages.
		private const int MaxPagePacketDelayMs = 150;

		public TeletextDecoder(TeletextService service)
		{
			_teletextService = service;
		}

		private void CheckLoadingPages()
		{
			if(DateTime.UtcNow.Subtract(_lastLoadingPagesCheck).TotalMilliseconds < (MaxPagePacketDelayMs / 10))
				return;

			_lastLoadingPagesCheck = DateTime.UtcNow;

			int magazineCount = _teletextService.Magazines.Length;
			for(int i = 0; i < magazineCount; i++)
			{
				TeletextMagazine magazine = _teletextService.Magazines[i];
				if(magazine == null)
					continue;

				TeletextPage loadingPage = magazine.LoadingPage;
				if(loadingPage == null)
					continue;

				if(DateTime.UtcNow.Subtract(loadingPage.LastPacketTimeUtc).TotalMilliseconds > MaxPagePacketDelayMs)
				{
					Log.Info(Config.Current.TeletextTraceSource, "Stop loading Mag={0}, Page={1:X} (forced).",
						magazine.Number, loadingPage.Header.PageAddress.PageNumber);
					magazine.FinishLoadingPage();
				}
			}
		}

		public void DecodePacket(TimeSpan presentationTime, byte[] buffer, int offset, int length)
		{
			if(length < TeletextPacketPayloadLength)
				throw new ArgumentOutOfRangeException("length");

			CheckLoadingPages();

			offset += (length - TeletextPacketPayloadLength);	// skip header if necessary

			Invert(buffer, offset, _buffer, 0, TeletextPacketPayloadLength);

			int magazineNumber;
			int packetNumber;
			DecodePacketAddress(_buffer, 0, out magazineNumber, out packetNumber);

			TeletextMagazine magazine;
			if(!_teletextService.TryGetMagazine(magazineNumber, out magazine))
			{
				magazine = new TeletextMagazine(_teletextService, magazineNumber);
				_teletextService.AddMagazine(magazine);
			}

			TeletextPage loadingPage = magazine.LoadingPage;
			if(packetNumber == 0)
			{
				PageHeaderPacket headerPacket = new PageHeaderPacket(magazineNumber, _buffer, 2);

				if(loadingPage != null)
				{
					Log.Info(Config.Current.TeletextTraceSource, "Stop loading Mag={0}, Page={1:X}.",
						magazine.Number, loadingPage.Header.PageAddress.PageNumber);
					magazine.FinishLoadingPage();
					loadingPage = null;
				}

				if(headerPacket.PageAddress.PageNumber != PageAddress.NullPageNumber)
				{
					Log.Info(Config.Current.TeletextTraceSource,
						"EBU teletext packet. Mag={0}, Page={1:X}, Row={2}. PTS={3}, " +
						"SubCode={4:X}, Errase={5}, Subs={6}, Charset={7}",
						magazineNumber, headerPacket.PageAddress.PageNumber, packetNumber, presentationTime,
						headerPacket.PageAddress.SubCode, headerPacket.ErasePage, headerPacket.Subtitle, headerPacket.NationalCharacterSubset);

					TeletextPage page;
					if(!magazine.TryGetPage(headerPacket.PageAddress, out page))
					{
						page = new TeletextPage(magazine, headerPacket, presentationTime);
					}
					else
					{
						page.Reuse(headerPacket, presentationTime);
					}

					magazine.StartLoadingPage(page);
				}
			}
			else if(packetNumber <= 25)	// 1 to 25
			{
				if(loadingPage != null)
				{
					Log.Info(Config.Current.TeletextTraceSource, "EBU teletext packet. Mag={0}, Page={1:X}, Row={2}, PTS={3}.",
						magazineNumber, loadingPage.Header.PageAddress.PageNumber, packetNumber, presentationTime);

					NormalPacket normalPacket = new NormalPacket(magazineNumber, packetNumber, _buffer, 2);
					loadingPage.AddPacket(normalPacket);
				}
				else
					Log.Warning(Config.Current.TeletextTraceSource,
						"Loading page not found. Mag={0}, Row={1}.", magazineNumber, packetNumber);
			}
			else if(packetNumber <= 28)	// 26 to 28
			{
				int designationCode = DecodeDesignationCode(_buffer, 2);

				if(loadingPage != null)
				{
					Log.Info(Config.Current.TeletextTraceSource, "EBU teletext packet. Mag={0}, Page={1:X}, Row={2}/{3}, PTS={4}.",
						magazineNumber, loadingPage.Header.PageAddress.PageNumber, packetNumber, designationCode, presentationTime);

					if(packetNumber == 26)
					{
						PacketX26 packet = new PacketX26(magazineNumber, packetNumber, _buffer, 2);
						loadingPage.AddPacket(packet);

						StringBuilder sb = new StringBuilder();
						for(int i = 0; i < packet.CharReplacements.Count; i++)
						{
							if(i > 0) sb.Append(", ");
							sb.Append(packet.CharReplacements[i]);
						}
						Log.Write(Config.Current.TeletextTraceSource, "CharReplacements: {0}.", sb);
					}
					else if(packetNumber == 27)
					{
						if(designationCode <= 3)
						{
							PacketX270 packet =
								new PacketX270(magazineNumber, packetNumber, _buffer, 2);
							loadingPage.AddPacket(packet);

							StringBuilder links = new StringBuilder();
							for(int i = 0; i < packet.Links.Length; i++)
							{
								links.Append(' ');
								links.Append(packet.Links[i]);
							}
							Log.Write(Config.Current.TeletextTraceSource, "Links:{0}.", links);
						}
					}
					else if(packetNumber == 28)
					{
						if(designationCode == 0)
						{
							PacketX280Format1 packet = new PacketX280Format1(magazineNumber, packetNumber, _buffer, 2);
							loadingPage.AddPacket(packet);
							
							Log.Write(Config.Current.TeletextTraceSource, "PageFunction={0}, PageCoding={1}.",
								packet.PageFunction.ToString(), packet.PageCoding.ToString());
							
							if(packet.PageFunction == PageFunction.BasicLevel1Teletext)
							{
								Log.Write(Config.Current.TeletextTraceSource, "Default charset={0}. Second charset={1}.",
									packet.BasicLevel1TeletextCoding.DefaultCharset,
									packet.BasicLevel1TeletextCoding.SecondCharset);
							}
						}
						else if(designationCode == 1)
						{
							PacketX281 packet = new PacketX281(magazineNumber, packetNumber, _buffer, 2);
							loadingPage.AddPacket(packet);
							
							Log.Write(Config.Current.TeletextTraceSource, "Default charset={0}. Second charset={1}.",
								packet.DefaultCharset, packet.SecondCharset);
						}
						else if(designationCode == 4)
						{
							PacketX284 packet = new PacketX284(magazineNumber, packetNumber, _buffer, 2);
							loadingPage.AddPacket(packet);

							Log.Write(Config.Current.TeletextTraceSource, "PageFunction={0}, PageCoding={1}.",
								packet.PageFunction.ToString(), packet.PageCoding.ToString());

							if(packet.PageFunction == PageFunction.BasicLevel1Teletext)
							{
								Log.Write(Config.Current.TeletextTraceSource, "Default charset={0}. Second charset={1}.",
									packet.BasicLevel1TeletextCoding.DefaultCharset,
									packet.BasicLevel1TeletextCoding.SecondCharset);
							}
						}

					}
				}
				else
					Log.Warning(Config.Current.TeletextTraceSource,
						"Loading page not found. Mag={0}, Row={1}.", magazineNumber, packetNumber);
			}
			else  // packetNumber = 29 to 31
			{
				int designationCode = DecodeDesignationCode(_buffer, 2);

				Log.Info(Config.Current.TeletextTraceSource, "EBU teletext packet. Mag={0}, Row={1}/{2}, PTS={3}.",
					magazineNumber, packetNumber, designationCode, presentationTime);

				if(packetNumber == 29)
				{
					if(designationCode == 0)
					{
						PacketM290 packet = new PacketM290(magazineNumber, packetNumber, _buffer, 2);
						magazine.AddMagazinePacket(packet);
						
						Log.Write(Config.Current.TeletextTraceSource, "PageFunction={0}, PageCoding={1}.",
							packet.PageFunction.ToString(), packet.PageCoding.ToString());
						
						if(packet.PageFunction == PageFunction.BasicLevel1Teletext)
						{
							Log.Write(Config.Current.TeletextTraceSource, "Default charset={0}. Second charset={1}.",
								packet.BasicLevel1TeletextCoding.DefaultCharset,
								packet.BasicLevel1TeletextCoding.SecondCharset);
						}
					}
					else if(designationCode == 4)
					{
						PacketM294 packet = new PacketM294(magazineNumber, packetNumber, _buffer, 2);
						magazine.AddMagazinePacket(packet);

						Log.Write(Config.Current.TeletextTraceSource, "PageFunction={0}, PageCoding={1}.",
							packet.PageFunction.ToString(), packet.PageCoding.ToString());

						if(packet.PageFunction == PageFunction.BasicLevel1Teletext)
						{
							Log.Write(Config.Current.TeletextTraceSource, "Default charset={0}. Second charset={1}.",
								packet.BasicLevel1TeletextCoding.DefaultCharset,
								packet.BasicLevel1TeletextCoding.SecondCharset);
						}
					}
					else
					{
						NonDisplayablePacket packet =
							new NonDisplayablePacket(magazineNumber, packetNumber, _buffer, 2);
						magazine.AddMagazinePacket(packet);
					}

				}
				else if(packetNumber == 30)
				{
					if(designationCode == 0 || designationCode == 1)
					{
						PacketS30Format1 packet = new PacketS30Format1(magazineNumber, packetNumber, _buffer, 2);
						_teletextService.AddServicePacket(packet);

						Log.Write(Config.Current.TeletextTraceSource, "Initial Page={0}.", packet.InitialPageAddress);
					}
					else if(designationCode == 2 || designationCode == 3)
					{
						PacketS30Format2 packet = new PacketS30Format2(magazineNumber, packetNumber, _buffer, 2);
						_teletextService.AddServicePacket(packet);

						Log.Write(Config.Current.TeletextTraceSource, "Initial Page={0}.", packet.InitialPageAddress);
					}
					else
					{
						//NonDisplayablePacket packet =
						//    new NonDisplayablePacket(magazineNumber, packetNumber, _buffer, 2);
						//_teletextService.AddServicePacket(packet);
					}
				}
				else if(packetNumber == 31)
				{
				}
			}

		}

		internal static int DecodeDesignationCode(byte[] buffer, int offset)
		{
			return HummingDecoder.Decode(buffer[offset]) & 0xF;
		}

		internal static void DecodePacketAddress(byte[] buffer, int offset, out int magazine, out int number)
		{
			byte magAndPacketNumber = HummingDecoder.Decode(buffer[offset], buffer[offset + 1]);
			magazine = magAndPacketNumber & 7;
			number = (magAndPacketNumber >> 3) & 0x1f; // packet number (Y)
		}

		internal static PageAddress DecodePageAddress(byte[] buffer, int offset)
		{
			return DecodePageAddress(buffer, offset, -1);
		}

		internal static PageAddress DecodePageAddress(byte[] buffer, int offset, int magazineNumber)
		{
			int pageNumber = HummingDecoder.Decode(buffer[offset], buffer[offset + 1]);
			int pageSubCode = ((HummingDecoder.Decode(buffer[offset + 4], buffer[offset + 5]) << 8) |
				HummingDecoder.Decode(buffer[offset + 2], buffer[offset + 3])) & 0x3F7F;
			if(magazineNumber == -1)
			{
				magazineNumber = ((buffer[offset + 3] & 0x80) >> 7) |	// M1
					((buffer[offset + 5] & 0x20) >> 4) |	// M2
					((buffer[offset + 5] & 0x80) >> 5);	// M3

			}
			return new PageAddress(magazineNumber, pageNumber, pageSubCode);
		}

		/// <summary>
		/// Source     0 1 0 1
		/// Complement 0 0 1 1
		/// Result     0 0 1 0
		/// </summary>
		/// <param name="data"></param>
		/// <param name="complement"></param>
		/// <returns></returns>
		internal static byte Complement(byte data, byte complement)
		{
			byte saved = (byte)(data & ~complement);
			byte inv = (byte)((~(data & complement)) & complement);
			return (byte)(saved | inv);
		}

		internal static void Invert(byte[] src, int srcOffset, byte[] dest, int destOffset, int count)
		{
			for(int i = srcOffset; count-- > 0; i++)
				dest[destOffset++] = _inversionTable[src[i]];
		}

		internal static string DecodeText(TeletextPage page)
		{
			StringWriter stringWriter = new StringWriter();
			TeletextTextWriter teletextWriter = new TeletextTextWriter(stringWriter);
			teletextWriter.Settings.EndRowWithNewLine = false;
			
			for(int i = 0; i <=24; i++)
				DecodeText(page, i, teletextWriter);

			teletextWriter.Flush();
			stringWriter.Flush();
			return stringWriter.ToString();
		}

		internal static string DecodeText(TeletextPage page, int row)
		{
			StringWriter stringWriter = new StringWriter();
			TeletextTextWriter teletextWriter = new TeletextTextWriter(stringWriter);
			teletextWriter.Settings.EndRowWithNewLine = false;
			
			DecodeText(page, row, teletextWriter);

			teletextWriter.Flush();
			stringWriter.Flush();
			return stringWriter.ToString();
		}

		internal static string DecodeTextToHtml(TeletextPage page)
		{
			StringWriter stringWriter = new StringWriter();
			TeletextHtmlWriter teletextWriter = new TeletextHtmlWriter(stringWriter);
			teletextWriter.Settings.EndRowWithNewLine = false;

			for(int i = 0; i <=24; i++)
				DecodeText(page, i, teletextWriter);

			teletextWriter.Flush();
			stringWriter.Flush();
			return stringWriter.ToString();
		}

		internal static string DecodeTextToHtml(TeletextPage page, int row)
		{
			StringWriter stringWriter = new StringWriter();
			TeletextHtmlWriter teletextWriter = new TeletextHtmlWriter(stringWriter);
			teletextWriter.Settings.EndRowWithNewLine = false;
			
			DecodeText(page, row, teletextWriter);

			teletextWriter.Flush();
			stringWriter.Flush();
			return stringWriter.ToString();
		}

		private static void DecodeText(TeletextPage page, int row, TeletextWriter writer)
		{
			byte[] data = page.GetRowData(row);
			
			TextEncoding defaultEncoding;
			TextEncoding secondEncoding;
			TextEncoding blockMosaicEncoding;
			TextEncoding smoothMosaicEncoding;
			GetPageEncodings(page, out defaultEncoding, out secondEncoding, out blockMosaicEncoding, out smoothMosaicEncoding);
			if(secondEncoding == null)
				secondEncoding = defaultEncoding;
			TextEncoding currentEncoding = defaultEncoding;

			writer.WriteStartRow(row);

			if(data != null)
			{
				RowState lastRowState = null;
				RowState rowState = new RowState();

				char[] chars = new char[1];
				byte holdMosaicChar = 0x20;
				int length = data.Length;
				int startPosition = TeletextPage.RowLength - length;
				for(int column = 0; column < TeletextPage.RowLength; column++)
				{
					byte ch = (byte)(column >= startPosition ? data[column - startPosition] & 0x7F : 0x20);
					byte chSource = ch;
					bool isCharReplaceable = (ch > 0x20);

					if(ch < 0x20)
					{
						bool wasHoldMosaic = rowState.HoldMosaic;
						rowState.UpdateBefore(ch);
						if(wasHoldMosaic || rowState.HoldMosaic)
							ch = holdMosaicChar;
						else
							ch = 0x20;
					}
					else
					{
						rowState.UpdateBefore(ch);
						if(rowState.MosaicMode && ((ch & 0x20) == 0x20))
							holdMosaicChar = ch;
					}

					// Start a new character block in case the formatting changes.
					if(rowState != lastRowState)
					{
						if(lastRowState != null)
							writer.WriteEndColumnBlock();
						writer.WriteStartColumnBlock(column, rowState);
						lastRowState = new RowState(rowState);
					}

					// Get and write character
					char finalChar;
					if(isCharReplaceable)
					{
						CharReplacement charReplacement = page.GetCharReplacement(new CharPosition(row, column));
						if(!charReplacement.IsEmpty)
						{
							if(charReplacement.IsCharsetReplacement)
								finalChar = currentEncoding.GetChar(charReplacement.CharCode, charReplacement.Charset);
							else
								finalChar = currentEncoding.GetChar(charReplacement.CharCode, charReplacement.DiacriticMark);
						}
						else
							finalChar = currentEncoding.GetChar(ch);
					}
					else
						finalChar = currentEncoding.GetChar(ch);

					writer.WriteChar(finalChar);

					// Update current encoding.
					if(chSource < 0x20)
					{
						bool wasMosaicMode = rowState.MosaicMode;
						rowState.UpdateAfter(chSource);
						if((wasMosaicMode && !rowState.MosaicMode) || (!wasMosaicMode && rowState.MosaicMode))
							holdMosaicChar = 0x20;

						if(chSource <= 0x7)	// Alpha Colour Codes ("Set-After")
							currentEncoding = defaultEncoding;
						else if(chSource == 0x1B)	// ESC ("Set-After")
							currentEncoding = (currentEncoding != defaultEncoding ? defaultEncoding : secondEncoding);
						else if(chSource >= 0x10 && chSource <= 0x17)	// Mosaic Colour Codes ("Set-After")
							currentEncoding = blockMosaicEncoding;
					}
					else
						rowState.UpdateAfter(chSource);
				}
				writer.WriteEndColumnBlock();
			}
			writer.WriteEndRow();
		}

		public static void GetPageEncodings(TeletextPage page,
			out TextEncoding defaultEncoding, out TextEncoding secondEncoding,
			out TextEncoding blockMosaicEncoding, out TextEncoding smoothMosaicEncoding)
		{
			Charset defaultCharset = page.DefaultCharset;
			if(defaultCharset == Charset.Empty) defaultCharset = page.Magazine.DefaultCharset;
			if(defaultCharset == Charset.Empty) defaultCharset = page.Magazine.Service.DefaultCharset;

			Charset secondCharset = page.SecondCharset;
			if(secondCharset == Charset.Empty) secondCharset = page.Magazine.SecondCharset;

			defaultEncoding = TextEncoding.GetEncoding(defaultCharset);
			secondEncoding = (secondCharset != Charset.Empty ? TextEncoding.GetEncoding(secondCharset) : null);
			blockMosaicEncoding = TextEncoding.GetEncoding(new Charset(defaultCharset, Charset.G1));
			smoothMosaicEncoding = TextEncoding.GetEncoding(new Charset(defaultCharset, Charset.G3));
		}
		
	}
}
